﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Mls
{
    public class UnknownCharacterException : Exception
    {
        public UnknownCharacterException(byte raw, string languageName)
            : base(string.Format("Unkown Character in file: '{0:X}' - Language: {1}", raw, languageName))
        {
        }
    }

    public class UnknownLanguageException : Exception
    {
        public UnknownLanguageException()
            : base("Uknown language")
        {
        }
    }

    public enum Token
    {
        EndOfFile,
        Character,
        Semicolon,
        NewLine,
        Delimiter,
        Embed,
        HardSpace,
        ReverseIndent,
        ZeroSpace
    };


    public enum Flags : byte
    {
        None = 0,
        UnderLine = 0x01,
        Strike = 0x02,
        Accented = 0x08
    };

    public class Character
    {
        private Token _type;
        private byte _raw;
        private Flags _flags;
        private Language _language;
        private Accents _accents;

        public Token Type { get { return _type; } }
        public byte Raw { get { return _raw; } }
        public Flags Flags { get { return _flags; } }
        public Language Language { get { return _language; } }
        public Accents Accents { get { return _accents; } }

        public bool IsUnderlined { get { return (_flags & Flags.UnderLine) == Flags.UnderLine; } }
        public bool IsStrike { get { return (_flags & Flags.Strike) == Flags.Strike; } }
        public bool IsAccented { get { return (_flags & Flags.Accented) == Flags.Accented; } }
        public bool IsDigit { get { return _raw >= (byte)'0' && _raw <= (byte)'9'; } }
        public bool IsArabicDigit { get { return _language.IsArabic && IsDigit; } }

        private Character(Token type)
        {
            _type = type;
            switch (type)
            {
                case Token.EndOfFile:
                    _raw = 0;
                    break;
                case Token.Semicolon:
                    _raw = (byte)SpecialCharacter.Semicolon;
                    break;
                case Token.NewLine:
                    _raw = (byte)SpecialCharacter.NewLine;
                    break;
                case Token.Delimiter:
                    _raw = (byte)SpecialCharacter.Delimiter;
                    break;
                case Token.Embed:
                    _raw = (byte)SpecialCharacter.Embed;
                    break;
                case Token.HardSpace:
                    _raw = (byte)SpecialCharacter.HardSpace;
                    break;
                case Token.ReverseIndent:
                    _raw = (byte)SpecialCharacter.ReverseIndent;
                    break;
                case Token.ZeroSpace:
                    _raw = (byte)SpecialCharacter.ZeroSpace;
                    break;
                default:
                    throw new InvalidProgramException("Unknown character type");
            }
            _flags = Flags.None;
            _language = new Language(Language.LanguageSettings.Default, Language.LanguageSettings.Default.LatinLanguageId);
            _accents = null;
        }

        public override bool Equals(object obj)
        {
            return obj is Character && (obj as Character)._raw == _raw && (obj as Character)._language.Equals(_language);
        }

        internal Character(Language.LanguageSettings settings, ushort token)
        {
            _type = ResolveToken(token, settings, out _raw, out _language, out _flags);
            _accents = null;
        }

        internal Character(Language.LanguageSettings settings, ushort token, ushort accents)
        {
            _type = ResolveToken(token, settings, out _raw, out _language, out _flags);
            _accents = new Accents(accents);
        }

        private enum SpecialCharacter : byte
        {
            NewLine = 0x80,
            ReverseIndent = 0xF5,
            HardSpace = 0xFC,
            ZeroSpace = 0xFD,
            Embed = 0xFA,
            Delimiter = 0xFB,
            Semicolon = 0x3B
        };

        public static Character Eof { get { return new Character(Token.EndOfFile); } }
        public static Character Delimeter { get { return new Character(Token.Delimiter); } }
        public static Character Embed { get { return new Character(Token.Embed); } }
        public static Character NewLine { get { return new Character(Token.NewLine); } }

        public static bool IsFollowedByAccents(ushort token) { return (ResolveFlags(token) & Flags.Accented) == Flags.Accented; }

        private static byte ResolveRawCharacter(ushort token) { return (byte)(token & 0xFF); }
        private static byte ResolveLanguage(ushort token) { return (byte)((token >> 8) & 0xF); }
        private static Flags ResolveFlags(ushort token) { return (Flags)((token >> 12) & 0xF); }

        public static ushort Encode(byte raw, byte languageId, Flags flags) { return (ushort)(raw + ((languageId & 0xF) << 8) + (((byte)flags & 0xF) << 12)); }

        public override string ToString()
        {
            return CharactersMapper.Map(this);
        }

        public ushort[] Encoded
        {
            get
            {
                ushort firstWord = Encode(_raw, _language.Id, _flags);
                if ((_flags & Flags.Accented) == Flags.Accented)
                    return new ushort[] { firstWord, _accents.Encoded };
                else
                    return new ushort[] { firstWord };
            }
        }

        private static Token ResolveToken(ushort token, Language.LanguageSettings settings, out byte raw,
            out Language language, out Flags flags)
        {
            raw = ResolveRawCharacter(token);
            language = new Language(settings, ResolveLanguage(token));
            flags = ResolveFlags(token);

            if (raw == 0)
                throw new UnknownCharacterException(raw, "N/A");
            /*else if (raw == 0xFF)
                throw new UnknownCharacterException(raw, "N/A");*/
            else if (raw == (byte)SpecialCharacter.Semicolon && language.IsLatin)
                return Token.Semicolon;
            else if (raw == (byte)SpecialCharacter.NewLine)
                return Token.NewLine;
            else if (raw == (byte)SpecialCharacter.Delimiter)
                return Token.Delimiter;
            else if (raw == (byte)SpecialCharacter.Embed)
                return Token.Embed;
            else if (raw == (byte)SpecialCharacter.HardSpace)
                return Token.HardSpace;
            else if (raw == (byte)SpecialCharacter.ReverseIndent)
                return Token.ReverseIndent;
            else if (raw == (byte)SpecialCharacter.ZeroSpace)
                return Token.ZeroSpace;
            else
                return Token.Character;
        }
    }

    public class Language
    {
        public enum Type
        {
            Latin,
            Coptic,
            Greek,
            Arabic
        }

        public class LanguageSettings
        {
            private byte _latinLanguageId;
            private byte _copticLanguageId;
            private byte _greekLanguageId;
            private byte _arabicLanguageId;

            public byte LatinLanguageId { get { return _latinLanguageId; } }
            public byte CopticLanguageId { get { return _copticLanguageId; } }
            public byte GreekLanguageId { get { return _greekLanguageId; } }
            public byte ArabicLanguageId { get { return _arabicLanguageId; } }

            public LanguageSettings(byte latinLanguageId, byte copticLanguageId, byte greekLanguageId, byte arabicLanguageId)
            {
                _latinLanguageId = latinLanguageId;
                _copticLanguageId = copticLanguageId;
                _greekLanguageId = greekLanguageId;
                _arabicLanguageId = arabicLanguageId;
            }

            public Type Resolve(int languageId)
            {
                if (languageId == _arabicLanguageId)
                    return Type.Arabic;
                else if (languageId == _copticLanguageId)
                    return Type.Coptic;
                else if (languageId == _greekLanguageId)
                    return Type.Greek;
                else if (languageId == _latinLanguageId)
                    return Type.Latin;
                else
                    throw new UnknownLanguageException();
            }

            public static LanguageSettings Default { get { return new Language.LanguageSettings(0, 2, 3, 1); } }
        }

        private byte _languageId;
        private LanguageSettings _settings;

        internal byte Id { get { return _languageId; } }

        internal Language(LanguageSettings settings, byte languageId)
        {
            _settings = settings;
            _languageId = languageId;
        }

        public override bool Equals(object obj)
        {
            return obj is Language && (obj as Language)._languageId == _languageId;
        }

        public Type LanguageType { get { return _settings.Resolve(_languageId); } }

        public bool IsArabic { get { return LanguageType == Type.Arabic; } }
        public bool IsGreek { get { return LanguageType == Type.Greek; } }
        public bool IsLatin { get { return LanguageType == Type.Latin; } }
        public bool IsCoptic { get { return LanguageType == Type.Coptic; } }
    }

    public class Accents
    {
        private const byte NumberOfAllowedAccents = 3;
        private const byte AccentCodeSize = 5; // In bits
        private const byte AccentWordMask = (byte)(1 << AccentCodeSize) - 1; // 0x1F

        private ushort _accents;

        internal ushort Encoded { get { return _accents; } }

        internal Accents(ushort accents)
        {
            _accents = accents;
        }

        public byte[] AccentIds
        {
            get
            {
                List<byte> ids = new List<byte>();

                // Loop for allowed accents, start from highest significant accent word
                for (int i = NumberOfAllowedAccents - 1; i >= 0; i--)
                {
                    byte accentId = (byte)((_accents >> (i * AccentCodeSize)) & AccentWordMask);

                    if (accentId == 0)
                        break;

                    ids.Add(accentId);
                }

                return ids.ToArray();
            }
        }
    }
}
